import { UNA_URL, UNA_API_KEY, MULTITENANT, PREVIEW_DOMAIN, appSetting } from 'app/config'; import { decodeTextNode } from 'app/lib/util/html-text'; import { cache } from 'react' import Root from 'app/root-client' import { Suspense } from 'react' import { notFound } from 'next/navigation' import { headers } from "next/headers" import { resolveLangCode } from '../../lib/resolve-lang' import { cachedUnaPageJson, unaPageCacheKey } from '../../lib/una-page-cache' const SITE_TITLE = appSetting('config', 'title'); async function loadPage(props) { const [params, searchParams] = await Promise.all([ props.params, props.searchParams, ]); return getData(params.path.join('/'), JSON.stringify(searchParams)); } // Primitive arguments let metadata and page rendering share this request's fetch. const getData = cache(async (path, searchParamsKey) => { const search_params = JSON.parse(searchParamsKey); // Early check for static files — before any logs or UNA requests const staticFileExtensions = ['.map', '.js', '.css', '.json', '.png', '.jpg', '.svg', '.ico', '.woff', '.woff2', '.ttf']; const isStaticFile = staticFileExtensions.some(ext => path.endsWith(ext)); if (isStaticFile || path.startsWith('_next/') || path.startsWith('static/')) { // Return 404 without logging or UNA requests return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: 404 }; } const hdrs = await headers(); // The viewer's Cookie header exactly as the browser sent it — forwarded to UNA. const cookieString = hdrs.get('cookie') || ''; // Tenant headers come from proxy.js only (it drops client-sent ones); trust them // only when the proxy can set them: multi-tenant mode or CI preview hosts. const trustTenantHeaders = MULTITENANT || !!PREVIEW_DOMAIN; const tenantUnaUrl = trustTenantHeaders ? hdrs.get('x-tenant-una-url') : null; const tenantUnaKey = trustTenantHeaders ? hdrs.get('x-tenant-una-key') : null; const fetchHeaders = { cookie: cookieString, authorization: 'Bearer ' + (tenantUnaKey ?? UNA_API_KEY), // The page is rendered for a NEO web viewer: UNA records site actions with this platform. 'x-neo-platform': 'web', }; let l = tenantUnaUrl ?? UNA_URL; l += '/api.php' + '?r=system/get_page_by_request/TemplServicePages¶ms[]=' + path; const langCode = resolveLangCode(cookieString, hdrs?.get('accept-language') || ''); if (langCode) { l += '&lang=' + encodeURIComponent(langCode); } let searchParams = { ...search_params }; delete searchParams.cookieString; // legacy: old proxy put cookies in the URL delete searchParams.path; delete searchParams._rsc; let sBlocks = ''; if (searchParams.blocks) { sBlocks = searchParams.blocks; } delete searchParams.blocks; if (sBlocks != '' && Object.keys(searchParams).length == 0) { l = l + '¶ms[]=' + sBlocks; } if (Object.keys(searchParams).length > 0) { l = l + '¶ms[]=' + sBlocks + '¶ms[]=' + JSON.stringify(searchParams); } const loadUnaPage = async () => { let res; try { res = await fetch(l, { headers: fetchHeaders, cache: 'no-store', }) } catch (error) { console.error('Server fetch failed (network/connection):', error); return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: 503 }; } /* if (!res.ok) { const body = await res.text().catch(() => ''); console.error('Server fetch failed (http):', res.status, body); return { data: { title: SITE_TITLE, description: SITE_TITLE }, code: res.status }; }*/ // Read the body once: a clone kept for the error branch buffered every page twice. let text = ''; try { text = await res.text(); return JSON.parse(text); } catch (error) { const fatal = text.match(/Fatal error[^<]*/i)?.[0] || text.match(/Uncaught [\w\\]+:[^\n<]+/)?.[0] || text.slice(0, 200); console.error('UNA page JSON parse failed:', fatal); return { data: { title: SITE_TITLE, description: SITE_TITLE, page_status: 503 }, code: 500, }; } }; return cachedUnaPageJson({ path, key: unaPageCacheKey({ url: l, tenant: tenantUnaUrl ?? '', cookieString: cookieString || '', }), load: loadUnaPage, }); }); export const viewport = { width: 'device-width', initialScale: 1, //maximumScale: 1, // Prevent iOS auto-zoom on input focus viewportFit: 'cover', themeColor: [ { media: '(prefers-color-scheme: light)', color: 'rgb(244,244,245)' }, { media: '(prefers-color-scheme: dark)', color: 'rgb(9,9,11)' }, ], } export async function generateMetadata(props) { const data = await loadPage(props); // UNA sends titles HTML-escaped; Next escapes metadata again, so the tab would read `&`. const description = decodeTextNode(data?.data?.description || SITE_TITLE); const name = decodeTextNode(data?.data?.title || SITE_TITLE); const image = data?.data?.image; const isClientProject = UNA_URL != 'https://api.neo.so'; const appleAppId = appSetting('smart_app_banner', 'apple_app_id'); const appleAppArgument = appSetting('smart_app_banner', 'apple_app_argument'); const other = { 'mobile-web-app-capable': 'yes', }; if (appleAppId) { other['apple-itunes-app'] = appleAppArgument ? `app-id=${appleAppId}, app-argument=${appleAppArgument}` : `app-id=${appleAppId}`; } return { title: name, description: description, manifest: isClientProject ? '/static/manifest.json' : '/manifest.json', icons: { icon: isClientProject ? '/static/favicon.svg' : '/favicon.svg', }, other, openGraph: { title: name, description: description, ...(image && { images: [ { url: image, alt: name, }, ], }), }, twitter: { card: 'summary_large_image', title: name, description: description, ...(image && { images: [image], }), }, } } export default async function Page(props) { const params = await props.params; const path = params?.path?.join('/') || ''; const isHomePage = path === '' || path === 'home' || path === 'index'; const data = await loadPage(props); // Static file 404 — call notFound immediately if (data?.code === 404 && !isHomePage) { notFound(); } // Handle API errors gracefully - especially for home/splash page // UNA may return errors for guest users when certain blocks (e.g., messenger) // try to access user context. The frontend should still render the splash page. const hasApiError = data?.code === 500 || data?.code === 503; if (hasApiError && isHomePage) { console.warn('UNA API error on home page - rendering with fallback data for splash screen'); // Provide minimal fallback data so splash page can render const fallbackData = { title: SITE_TITLE, description: SITE_TITLE, uri: '/', url: '/', page_name: 'home', page_type: 'home', logged: 0, // Empty blocks - splash will render static content blocks: {} }; return ( ); } if (data?.data?.page_status == 404) { notFound(props) } return ( ) }